Skip to content

Running on Mount

Video Summary

In this video, we explore how we might build an “auto-focusing” text input, one that captures focus when the component mounts.

Suppose we have the following code:

function App() {
const [
searchTerm,
setSearchTerm,
] = React.useState('');
return (
<main>
<form>
{/* I want to focus this input on mount: */}
<input
value={searchTerm}
onChange={(event) => {
setSearchTerm(event.target.value);
}}
/>
<button>Search</button>
</form>
</main>
);
}

We can capture a reference to that input with the useRef hook, as we saw earlier:

function App() {
const [
searchTerm,
setSearchTerm,
] = React.useState('');
const inputRef = React.useRef();
return (
<main>
<form>
<input
ref={inputRef}
value={searchTerm}
onChange={(event) => {
setSearchTerm(event.target.value);
}}
/>
<button>Search</button>
</form>
</main>
);
}

Input DOM nodes have a .focus() method we can call to focus it, but how do I do it on mount?

I can try to do it right in the render:

function App() {
const [
searchTerm,
setSearchTerm,
] = React.useState('');
const inputRef = React.useRef();
inputRef.current.focus();
// ✂️
}

Unfortunately, this leads to an error:

Cannot read properties of undefined (reading 'focus').

The trouble is that when we first create the inputRef, it's empty ({ current: undefined }). It only captures the input DOM node after the first render.

The solution is to use the useEffect hook:

function App() {
const [
searchTerm,
setSearchTerm,
] = React.useState('');
const inputRef = React.useRef();
React.useEffect(() => {
inputRef.current.focus();
}, []);
// ✂️
}

Critically, we're passing an empty dependency array. This is how we tell React, “this effect doesn't depend on any other values”. And if it doesn't depend on any values, it'll never re-run!

Effects always run after the first render, and then again whenever the dependencies change. This structure ensures it'll only run after the first render.

Here's the sandbox from the video. Uncomment the line in the effect to pull your focus:

Code Playground

import React from 'react';

function App() {
const [
searchTerm,
setSearchTerm,
] = React.useState('');

const inputRef = React.useRef();

React.useEffect(() => {
// Uncomment me!
// inputRef.current.focus();
}, []);

return (
<>
<header>
<img
className="logo"
alt="Foobar"
src="https://sandpack-bundler.vercel.app/img/foogle.svg"
/>
</header>
<main>
<form>
<input
ref={inputRef}
value={searchTerm}
onChange={(event) => {
setSearchTerm(event.target.value);
}}
/>
<button>Search</button>
</form>
</main>
</>
);
}

export default App;

Subscriptions

Let's suppose we want to track the user's cursor position. Whenever they move their mouse, we'll update some state.

We can add onMouseMove event handlers to specific DOM nodes, like this:

<div
onMouseMove={event => {
setMousePosition({
x: event.clientX,
y: event.clientY,
});
}}
>

This will only work while the user is hovering over this particular <div>, though… What if we want to track their cursor position no matter where the mouse is within the viewport?

Spend a few moments tinkering, if you'd like, to see if you can come up with a solution. A sandbox has been provided:

Code Playground

import React from 'react';

function MouseCoords() {
const [
mousePosition,
setMousePosition
] = React.useState({ x: 0, y: 0 });
return (
<div className="wrapper">
<p>
{mousePosition.x} / {mousePosition.y}
</p>
</div>
);
}

export default MouseCoords;

Let's talk through it:

Video Summary

To listen for global mouse-move events, we need to use window.addEventListener. We can register it in an effect hook, like this:

React.useEffect(() => {
function handleMouseMove(event) {
setMousePosition({
x: event.clientX,
y: event.clientY,
});
}
window.addEventListener('mousemove', handleMouseMove);
});

At first glance, this seems to work, but there are two problems with this approach.

First, we aren't ever cleaning up our event listener. Let's put that one on the back burner for now; we'll talk about cleanup in the next lesson.

The issue I want to talk about is that we're adding multiple event listeners.

Because we haven't specified a dependency array, this effect will run after every single render. That means every time the user's mouse position changes, we call window.addEventListener again. If 100 mouse-move events fire, we'll have 100 event listeners.

window.addEventListener is a subscription. We only want to subscribe once, when the component first mounts.

Here's what the proper solution looks like:

React.useEffect(() => {
function handleMouseMove(event) {
setMousePosition({
x: event.clientX,
y: event.clientY,
});
}
window.addEventListener('mousemove', handleMouseMove);
}, []);

window.addEventListener is not part of React, it's part of the DOM. When we call this method, we set up a long-running process that will call our callback function whenever the mousemove event is detected.

It's the same story with many other situations, like:

  • Running an interval
  • Opening a web socket connection
  • Using ResizeObserver

I have some illustrations that show visually what's going on here.

With an empty dependency array, the effect only runs once, after the first render, starting a single long-running process:

Illustration showing how the effect only runs after the first render, starting a long-running process

Without the empty dependency array, however, our effect runs after every render, starting multiple long-running processes:

Illustration showing how the effect only runs after the first render, starting a long-running process

Here are the diagrams from the video:

With an empty dependency array:

Illustration showing how the effect only runs after the first render, starting a long-running process

With no dependency array:

Illustration showing how the effect only runs after the first render, starting a long-running process

And here's the final solution from the video:

Code Playground

import React from 'react';

function MouseCoords() {
const [mousePosition, setMousePosition] = React.useState({
x: 0,
y: 0,
});

React.useEffect(() => {
function handleMouseMove(event) {
setMousePosition({
x: event.clientX,
y: event.clientY,
});
}

window.addEventListener('mousemove', handleMouseMove);
}, []);

return (
<div className="wrapper">
<p>
{mousePosition.x} / {mousePosition.y}
</p>
</div>
);
}

export default MouseCoords;